# Array (Data Structures) - javatpoint
# https://www.javatpoint.com/data-structure-array
"""
Definition:
An array is a linear data structure used to store a sequence of elements of the same data type. It is composed of contiguous memory locations, where each location has a unique index used to access the corresponding element. The size of an array is determined at the time of creation and is typically fixed during runtime.

Properties:
1. Size: The size of an array represents the number of elements it can accommodate, which is determined when the array is created.
2. Element Type: The elements stored in an array have a fixed data type, and all elements must be of the same type, such as integers, floating-point numbers, characters, etc.
3. Index: Each element in the array has a unique index that is used to access that element. Indexing typically starts from 0 and increments sequentially.

Operations:
1. Access: Elements in an array can be accessed using their respective indices. For example, arr[i] denotes accessing the i-th element in the array.
2. Insertion: It is possible to insert a new element at a specific position in the array. Inserting an element may require shifting the subsequent elements to make space.
3. Deletion: Elements in the array can be removed at a specified position. Deleting an element requires shifting the subsequent elements to fill the gap.
4. Update: Elements at a specific position in the array can be modified.
5. Traversal: Array traversal involves accessing all elements in the array sequentially.

Complexity:
1. Access: O(1) - Array elements are stored contiguously, allowing direct computation of the memory address of an element based on its index. Hence, accessing an element has a constant time complexity.
2. Insertion (at a specified position): Best case O(1) (insertion at the end of the array), worst case O(n) (insertion at the beginning of the array), average case O(n).
3. Deletion (at a specified position): Best case O(1) (deletion at the end of the array), worst case O(n) (deletion at the beginning of the array), average case O(n).
4. Update: O(1) - Modifying the value of an element can be done by directly accessing the element using its index, resulting in constant time complexity.
5. Traversal: O(n) - To traverse all elements in the array, each element needs to be accessed once in a sequential manner. Therefore, the time complexity is linear, directly proportional to the size of the array.

It's important to note that insertion and deletion operations in an array may involve shifting a significant number of elements, so arrays may not be the optimal choice if frequent insertions or deletions are required.
"""

class Array:
    """
    A simple array class that wraps around a list.

    This array class supports insertion, deletion, and random access operations.
    The data in the array can be of any type.

    Attributes:
        capacity (int): The maximum capacity of the array.
    """

    def __init__(self, capacity: int = 10):
        """
        Initialize the array.

        Args:
            capacity (int, optional): The maximum capacity of the array. Defaults to 10.
        """
        self._data = []
        self._capacity = capacity

    def __len__(self) -> int:
        """Return the number of elements in the array."""
        return len(self._data)

    def __iter__(self):
        """Yield each item in the array."""
        for item in self._data:
            yield item

    def __repr__(self):
        """Return a string representation of the array."""
        return f'Array(capacity={self._capacity}, data={self._data})'

    def __str__(self):
        """Return a formatted string representation of the array."""
        return '[' + ', '.join(str(item) for item in self._data) + ']'

    def __getitem__(self, index: int) -> object:
        """
        Get the item at a specific index in the array.

        Args:
            index (int): The index of the item.

        Raises:
            IndexError: If the index is out of range.

        Returns:
            object: The item at the specific index.
        """
        if index < 0 or index >= len(self):
            raise IndexError('Array index out of range')
        return self._data[index]

    def __setitem__(self, index: int, value: object):
        """
        Set the value of the item at a specific index in the array.

        Args:
            index (int): The index of the item.
            value (object): The new value.

        Raises:
            IndexError: If the index is out of range.
        """
        if index < 0 or index >= len(self):
            raise IndexError('Array index out of range')
        self._data[index] = value

    def __delitem__(self, index: int):
        """
        Delete the item at a specific index in the array.

        Args:
            index (int): The index of the item.

        Raises:
            IndexError: If the index is out of range.
        """
        if index < 0 or index >= len(self):
            raise IndexError('Array index out of range')
        del self._data[index]

    def insert(self, index: int, value: int) -> bool:
        """
        Insert a value at a specific index in the array.

        Args:
            index (int): The index where the value should be inserted.
            value (int): The value to insert.

        Returns:
            bool: True if the insertion was successful, False otherwise.
        """
        if index < 0 or index > len(self) or len(self) >= self._capacity:
            return False
        else:
            self._data.insert(index, value)
            return True


if __name__ == "__main__":
    array = Array(5)  # Initialize an array with capacity 5

    # Inserting elements into the array
    array.insert(0, 3)  # Expected output: True, the array becomes [3]
    array.insert(1, 5)  # Expected output: True, the array becomes [4, 3]
    array.insert(2, 6)  # Expected output: True, the array becomes [4, 5, 3]
    array.insert(3, 9)  # Expected output: True, the array becomes [4, 5, 3, 9]
    array.insert(4, 10)  # Expected output: True, the array becomes [4, 5, 3, 10, 9]

    # Try to insert an element when the array is already at capacity
    assert not array.insert(0, 100), "Expected insertion to fail because array is at capacity"

    print(array)  # Expected output: Array(capacity=5, data=[4, 5, 3, 10, 9])
    assert len(array) == 5, "Expected array length to be 5"
    assert array[1] == 5, "Expected element at index 1 to be 5"
    assert array[4] is not None, "Expected element at index 4 to exist"

    # Delete the first element
    del array[0]  # The array becomes [5, 3, 10, 9]
    print(array)  # Expected output: Array(capacity=5, data=[5, 3, 10, 9])

    # Change the element at index 3
    array[3] = 7  # The array becomes [5, 3, 10, 7]
    print(array)  # Expected output: Array(capacity=5, data=[5, 3, 10, 7])
